## RaceTrack.py


import app.Physical as Physical
from app.Physical import Accuracy, AccuracyUnit, Measurement, tstamp

import app.PassData as PassData
import app.Database as Database
from   app.Database import FieldDecl
import datetime

from collections import namedtuple


## ###################################################################################################################
## ###################################################################################################################
## ###################################################################################################################

## to store/load the pysical properties of a RaceTrack
##
##  physical dimensions
##
##    aul -- accuracy unit list
##
##                 AccuracyUnit
##                 acc Accuracy      unit
##                 neg       pos
##         index   ----------------- -----------
##           0   | (0,-0.1)  (0,0.1) 'ft,in'
##           1   | -0.005    0.005   'in'
##
##    ail -- accuracy index list -- dll rows (below) share the same set of aul
##
##               [ 0                 1       1   ]
##
##    dll -- distance list of lists -- each column accuracy defined via ail 
##
##       station   data list
##       number    -------------------------------
##           0   | (15.0*0,0+  0 ) , 1.020 , 1.035
##           1   | (15.0*1,0+1/8 ) , 1.045 , 1.065
##           2   | (15.0*2,0+3/16) , 1.005 , 1.060
##           3   | (15.0*3,1+ 0  ) , 1.020 , 1.050
##           4   | (15.0*4,1+1/8 ) , 1.035 , 1.055
##           5   | (15.0*5,0+5/16) , 1.050 , 1.040
##
##  timing dimensions
##
##    nta -- network time accuracy
##
##         AccuracyUnit
##         Accuracy
##         neg   pos   unit
##         ---------   -----------
##          -2   0     'tick,rate'
##
##    ntr -- network tick rate
##
##         125000.000
##
##  packet data conversion
##
##    lex_style -- packet lexer style
##
##         'Arduino2014'
##
##    recv_style -- packet source
##
##         'Arduino2014'
##


RaceTrackProperties = namedtuple( 'RaceTrackProperties', [
    'aul', # accuracy unit list -- a list of AccuracyUnit records
    'ail', # accuracy index list -- a list of integer indices into aul
    'dll', # distance list of lists -- each list describes the distance between
    'nta', # network time accuracy -- AccuracyUnit for time accuracy of the sensors
    'ntr', # network tick rate -- in ticks per second
    'lex_style', # -- packet style -- string of know styles -- used to index PacketProcess.lex_func
    'recv_style'  # where new packets come from -- used to index PacketProcess.recv_func
    ]
    )
    # this describes an entire track
    # supposedly it can be stored in a database
    # -- although i'm not sure exactly how yet

## we can store these layouts in a database using the FieldDecl and transformer_dictionary
racetrack_database = [
    FieldDecl( 'timestamp', 'REAL' ),
    FieldDecl( 'trackname', 'TEXT' ),
    FieldDecl( 'racetrackproperties','RaceTrackProperties' )
    ]
# define the named tuple
RacetrackDatabase = Database.make_namedtuple( 'RacetrackDatabase',racetrack_database )


## attach transformers
def RaceTrackProperties_adapter(rtp) :
    """ must return a byte string so use encode('ascii') """
    ret = str(rtp).encode('ascii')
    #print( "    RaceTrackProperties_adapter --> ", type(ret), ret, flush=True )
    return ret
def RaceTrackProperties_converter(s) :
    """ input s is a byte string so prefix strings with b """
    #print( "    RaceTrackProperties_converter" , type(s), s, flush=True )
    assert(s.startswith(b"RaceTrackProperties"))
    return eval(s) # DANGEROUS !!!!

racetrack_transformer_dictionary = {
    'RaceTrackProperties' : (RaceTrackProperties, RaceTrackProperties_adapter, RaceTrackProperties_converter )
    }
Database.attach_transformers( racetrack_database, racetrack_transformer_dictionary )

## assure_table_exists and create it if it doesnt

racetrack_database_name = 'Racetrack' # should be part of the class

Database.assure_table_exists(racetrack_database_name,racetrack_database )


## ###################################################################################################################
## ###################################################################################################################
## ###################################################################################################################

## for default, and because we haven't kept very good track of this stuff
## we have a default track that goes back to the origonal

default_racetrack_timestamp = tstamp( datetime.datetime(year=2009,month=6,day=15) )
default_racetrack_name = 'default_racetrack'
default_racetrack_properties = RaceTrackProperties (
    aul = [ AccuracyUnit(Accuracy( (0,-0.5),(0,0.5)),'ft,in' ),   # reduced accuracy
            AccuracyUnit(Accuracy(  -0.05,    0.05)  ,'in'    ) ] ,   # reduced accuracy
    ail = [ 0,1,1 ] ,
    dll = [ [  ( 0.0, 0),  1.0 , 1.0 ] ,
            [  (15.0, 0),  1.0 , 1.0 ] ,
            [  (15.0, 0),  1.0 , 1.0 ] ,
            [  (15.0, 0),  1.0 , 1.0 ] ,
            [  (15.0, 0),  1.0 , 1.0 ] ,
            [  (15.0, 0),  1.0 , 1.0 ]    ] ,
    nta = AccuracyUnit(Accuracy( -2,0 ),'tick,rate' ) ,
    ntr = 125000.0 , # network tick rate
    lex_style = 'Arduino2014' ,   # packet style
    recv_style = 'Arduino2014'     # packet source
    )

Database.assure_record_exists( racetrack_database_name, racetrack_database,
    default_racetrack_timestamp, default_racetrack_name, default_racetrack_properties )

## for code dev, we create some fixed values from old and untrusted cal data
## -- see TestPage2011.html
## -- x0 values are made up -- assume about 15 feet between stations

dev_racetrack_timestamp = tstamp( datetime.datetime(year=2011,month=6,day=15) )
dev_racetrack_name = 'dev_racetrack'
dev_racetrack_properties = RaceTrackProperties (
    aul = [ AccuracyUnit(Accuracy((0,-0.1),(0,0.1) ), 'ft,in' ),
            AccuracyUnit(Accuracy( -0.005,    0.005 ), 'in'    ) ] ,
    ail = [ 0,1,1 ] ,
    dll = [ [  ( 0.0, 0+ 0.0),  1.020 , 1.035 ] ,# the 0.0 is a dev identifier
            [  (15.0, 0+1/8 ),  1.045 , 1.065 ] ,
            [  (15.0, 0+3/16),  1.005 , 1.060 ] ,
            [  (15.0, 1+ 0  ),  1.020 , 1.050 ] ,
            [  (15.0, 1+1/8 ),  1.035 , 1.055 ] ,
            [  (15.0, 0+5/16),  1.050 , 1.040 ]    ] ,
    nta = AccuracyUnit(Accuracy( -2,0 ),'tick,rate' ) ,
    ntr = 125000.0 , # network tick rate
    lex_style = 'Arduino2014' ,   # packet style
    recv_style = 'FakeCar'     # packet source
    )

Database.assure_record_exists( racetrack_database_name, racetrack_database,
    dev_racetrack_timestamp, dev_racetrack_name, dev_racetrack_properties )




#dump so we can see what's going on ...
#Database.dump_table(racetrack_database_name)
#print()
#print()
#quit()

## ###################################################################################################################
## ###################################################################################################################
## ###################################################################################################################


# this is the fantasy race track
# it has pairs of sensors every 2 feet

dev_fantasy_racetrack_timestamp = tstamp( datetime.datetime(year=2014,month=5,day=1) )
dev_fantasy_racetrack_name = 'dev_fantasy_racetrack'
dev_fantasy_racetrack_properties = RaceTrackProperties (
    aul = [ AccuracyUnit(Accuracy( -1/32,   1/32  ), 'in' ),
            AccuracyUnit(Accuracy( -0.0025, 0.0025 ), 'in' ) ] ,
    ail = [ 0,1 ] ,
    dll = [ [ 0.0 , 1.0 ] ] + [ [ 24.0, 1.0 ] ] * 49 , # that's 49 copies
    nta = AccuracyUnit(Accuracy( -2,0 ),'tick,rate' ) ,
    ntr = 500000.0 , # network tick rate
    lex_style = 'Fantasy2014' ,   # packet style
    recv_style = 'FakeCar'     # packet source
    )

Database.assure_record_exists( racetrack_database_name, racetrack_database,
    dev_fantasy_racetrack_timestamp,
    dev_fantasy_racetrack_name,
    dev_fantasy_racetrack_properties )




## ###################################################################################################################
## ###################################################################################################################
## ###################################################################################################################

## no longer used -- but the data lists are the same shape and meaning
#    """ a StationDistance is an abstract collection of distances to be used with a StationTimeData
#        it's values are assumed to loaded by a RaceTrack
#        the number of dist elements in a StationDistance must match the number of time elements in a StationTime
#        the dx field is always present and represents the reletive offset from sensor 0 of the previous station
#        the dxji fields are the distances between in station sensors
#        numbering of dx entries is in acedemic style 'to'-'from' subscript
#            ex: dx21 means "the distance to sensor 2 starting from sensor 1
#        mechanical layout:
#            prev station <-- ... ---->|<----------------> to next station
#            station          ...      dx
#                             ...      |
#            sensors          ...     (0)        (1)        (2)
#            dxji values      ...      |<--dx10-->|<--dx21-->|<-- ...
#    """



## ###################################################################################################################
## ###################################################################################################################
## ###################################################################################################################

class RaceTrack :
    """ A RaceTrack holds the physical dimensions of a track,
        the communication format of packets and
        ... a bunch of other crap i'm not finished with yet
        It must be constructed with a timestamp and a name.
        The Racetrack class will summon the appropriate configuration from the database.
    """

    ## ignore this until rewritten
    #    time_accuracy = Accuracy( neg= -network_clock_to_second( 1 ), pos=0 )
    #    station_distance = Calibration.calibration_to_station_distance( Calibration.dev_calibration )
    ## note to self -- this is known to work
    #    NUM_STATIONS = len(station_distance) # should be 6
    #    SENSORS_PER_STATION = len( station_distance[0] ) # should be 3
    #    MAX_PACKET_CODE = (1<<SENSORS_PER_STATION)-1


    def __init__(self,timestamp,track_name) :
        """ name must exist in the database
            in order to get rtp -- RaceTrackProperties
        """
        # look up the name in the database
        # rtdb will be a RaceTrackDatabase
        #-- Database should do that for us !

        #print("with given time stamp ",timestamp,"AND name", track_name)
        rtdb = RacetrackDatabase._make( Database.get_recent_timestamp(
            racetrack_database_name,
            racetrack_database,
            timestamp,track_name ) )

        rtp = rtdb.racetrackproperties

        # set our own name
        self.name = track_name

        # set up the network rate and accuracy
        self.tick_rate = rtp.ntr
        self.time_accuracy = Physical.Accuracy_from_AccuracyUnit( rtp.nta, rtp.ntr )

        # convert AccuracyUnitList aul into Accuracy list al
        al = list( map( Physical.Accuracy_from_AccuracyUnit, rtp.aul ) )

        # now transform all the distance list of lists
        # by applying the appropriate accuracy to the values in dll
        self.station_distance_list = []    # a list of lists of sensor positions -- see above
        for dl in rtp.dll :   # dl is a line of distance list
            sd = [] # list of args for StationDistance -- each will be a Measurement
            for (i,d) in enumerate(dl) : # sensor_number, distance_value
                ai = rtp.ail[ i ] # accuracy format index for this column i
                x = Physical.convert( d, rtp.aul[ai].unit, 'SI' )
                sd.append( Measurement( x, al[ai] ) )
            self.station_distance_list.append( sd )

        # save our lex,recv styles
        self.recv_style = rtp.recv_style
        self.lex_style = rtp.lex_style






    def new_station_time_list(self) :
        """ returns a properly shaped list of None values to be filled in with time values from PacketProcess
            filled in stl can be passed to the velocity function
        """
        stl = []
        for sd in self.station_distance_list :
            stl.append( [None]*len(sd) )
        return stl

    def new_stl(self) :  return self.new_station_time_list()
    def get_sdl(self) :  return self.station_distance_list




    def velocity(self,units,stl,index) :
        """ Returns a Measurement of velocity at station[index]
                use ret.nom to get the nominal value
                or ret.lo ret.hi to get estimated error boundaries
            units must be one of the known velocity units (i.e. 'mph' or 'mps' )
            stl is a station time list
            index is the index of the desired station
                if index is an integer, station velocity is returned.
                if index is a tuple(i,j), average velocity between i and j is returned.
                if index is None, a list of all station velocities is returned.
        """
        # take care of the 'just gimme a damn list' form of the call
        if index is None :
            ret = []
            for i in len(self.station_distance_list) :
                ret.append( self.velocity(stl,i) )
            return ret

        # helper functions for the next two styles of calls
        def velocity_2( dt, dx ) : return dx/dt
        def velocity_3( dt10, dt21, dx10, dx21 ) : return ( dx10*dt21*dt21 + dx21*dt10*dt10 ) / ( dt10*dt21*(dt10+dt21) )

        # list of args for velocity function
        arg_t = []  # of type Measurement in seconds
        arg_x = []  # of type Measurement in meters

        sdl = self.station_distance_list # make this a shorter name please

        # now we have a known index, see if it's a tuple
        if isinstance(index,tuple) :
            assert( len(index)==2 ) # only two index values
            (i,j) = index # unpack
            # accumulate distance and time between
            # we want the first valid record at the 'leading' edge of the station
            # if either end has no valid sensors, then return None
            ## i<----------------------------->|<------...-->|<------...--->j-------------------------...
            ## dxi-->dx10-->dx21-->dx31-->...  dx1-->dx10.   dx2-->dx10..-->dxj-->dx10-->dx21-->dx31-->...
            ## t0   t1      t2     t3          t0   t1 ...   t0   t1 ...    t0   t1      t2     t3
            ##                                         ...           ...
            ## dx=0                            dx+=dx1  ...  dx+=dx2 ...    dx+=dxj
            ##      dx-=dx10                                                     dx+=dx10
            ##              dx-=dx21                                                     dx+=dx21
            ## while dt is None                                              until t is not None

            #print(flush=True)
            #print("velocity i={} stl[i]={}".format(i,stl[i]), flush=True)
            #print("velocity j={} stl[j]={}".format(j,stl[j]), flush=True)

            dx = Measurement(0,Accuracy(0,0))

            def find_first_stl(k,dir) :
                nonlocal dx
                (n,t) = (0, stl[k][0] )
                while t is None :
                    n += 1
                    if n>=len(stl[k]) : break
                    #print( "veocity k={},n={},stl[k]={}".format(k,n,stl[k]), flush=True)
                    t = stl[k][n] # try to set the new time
                    if   dir==-1 : dx -= sdl[k][n] # accumulate the between sensor dist
                    elif dir== 1 : dx += sdl[k][n] # accumulate the between sensor dist
                    else : assert(False) # bonehead ! pass in the proper value !
                return t

            #find the first valid t in [i] -- acumulate dx negative -- because we're closer to the target x0
            t0 = find_first_stl(i,-1)
            if t0 is None : return None

            # accumulate dx until [j]  -- acumulate dx positive -- because we're farther from the start
            for x in sdl[i+1:j+1] : dx += x[0]

            # find the first valid t in j -- acumulate dx positive -- because we're farther from the start
            t1 = find_first_stl(j, 1)
            if t1 is None : return None

            # now we can append args
            arg_t.append( t1 - t0 )
            arg_x.append( dx )

        else : # otherwise we are a plain old station index

            # define st and sd for the algorithm
            sd = sdl[index]
            st = stl[index]
            assert( len(sd) == len(st) ) # supposed to be the same size

            # peruse the sensors, looking for a valid combination

            # this is a little mind boggling, here's some charts

            # if none of the t's are None
            ## dist x0 dx10  dx21  dx32 ...   no x can be None
            ## self t0     t1    t2    t3 ... any t can be None
            ## arg     dt10  dt21  dt32 ...
            ##         dx10  dx21  dx32

            # if the first t is None, then advance
            ## dist x0 dx10  dx21  dx32 ...   no x can be None
            ## self None   t1    t2    t3 ... any t can be None
            ## arg           dt21  dt32 ...
            ##               dx21  dx32

            # if second t0 is none -- skip and accumulate
            ## dist x0 dx10  dx21  dx32 ...   no x can be None
            ## self t0    None   t2    t3 ... any t can be None
            ## arg           dt20  dt32 ...
            ##         dx10+dx21   dx32

            lst_t = None    # of type float -- last valid time we saw
            lst_x = Measurement(0,Accuracy(0,0)) # of type Measurement, last distance we worked with
            for (t,x) in zip( st, sd ) :

                if lst_t is None : # still looking for the first one
                    lst_t = t       # now we maybe have have a valid lst_t
                elif t is None :    # have a valid last, is the current one valid ?
                    lst_x += x      # no, so accumulate dx
                else : # have a valid pair
                    arg_t.append( t - lst_t )
                    arg_x.append( x + lst_x )
                    lst_t = t
                    lst_x = Measurement(0,Accuracy(0,0)) # reset last_x

        # now we can determine the approprate function
        if    len(arg_t) == 1 : func = velocity_2
        elif  len(arg_t) == 2 : func = velocity_3
        else: return None # can't do anything

        # and route to measurement
        meas = Physical.Measurement_from_function( func, arg_t + arg_x )
        if meas is None : return None

        # convert to caller unit preference
        return meas.convert( fr='mps',to=units )


## #########################################################################
## #########################################################################
## #########################################################################

## common database API

def Store(rt) :
    """ store to the database
        is that really necessary ?
    """
    pass

def Fetch(timestamp,track_name) :
    """ load from the database
        is that really necessary ?
    """
    pass
